public class WeChatUtil {
    private final static Logger log = LoggerFactory.getLogger(WeChatUtil.class);

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    private static final OkHttpClient OKHTTP_CLIENT = new OkHttpClient.Builder().build();

    //缓存token-key
    private static final String CACHE_KEY = "WECHAT_TOKEN_CACHE_KEY";
    private static final String TOKEN_KEY = "ACCESS_TOKEN";

//    private final Config config = ConfigKit.me().get("WeChatUtil.Config",Config.class);

    /**
     * 发送文本消息
     *
     * @param content   消息内容，最长不超过2048个字节，超过将截断
     * @param userIds 指定接收消息的成员，成员ID列表 多个接收者用‘|’分隔，最多支持1000个 指定为"@all"，则向该企业应用的全部成员发送
     * @return 是否发送成功
     */
    public static synchronized boolean sendMsg(Config config, String content, String... userIds) {
        WorkAccessToken token = getOrFlushToken(config);
        if (token == null) {
            log.info("发送企业微信消息-获取 TOEKN 为空!");
            return false;
        }
        //构建发送消息
        JSONObject postParam = new JSONObject().fluentPut("touser", StringUtils.join(Arrays.asList(userIds), "|"))
                .fluentPut("msgtype", "text")
                .fluentPut("agentid", config.getAgentId())
                .fluentPut("text", new JSONObject() {{
                    put("content", content);
                }});

        String msgUrl = String.format(config.getMessageSendUrl(), token.getAccessToken());
        try (
                Response response = OKHTTP_CLIENT.newCall(new Request.Builder()
                        .post(RequestBody.create(postParam.toJSONString(), MediaType.parse("application/json;charset=utf-8")))
                        .url(msgUrl)
                        .build()).execute()
        ) {
            ResponseBody body = response.body();
            if (Objects.isNull(body)) {
                log.info("发送企业微信消息-返回结果为空!");
                return false;
            }
            String resp = body.string();
            log.info("messageSendUrl:{}", config.getMessageSendUrl());
            log.info("msgUrl:{}", msgUrl);
            log.info("postParam:{}", postParam.toJSONString());
            log.info("res:{}", resp);
            if (!StringUtils.isNotBlank(resp)) {
                log.info("发送企业微信消息-返回结果为空!");
                return false;
            }
            try {
                WorkMsgResp workResp = OBJECT_MAPPER.readValue(resp, WorkMsgResp.class);
                return workResp != null && workResp.isSuccess();
            } catch (JsonProcessingException e) {
                log.error("发送企业微信消息时异常!", e);
                return false;
            }
        } catch (Exception e) {
            log.error("发送企业微信消息时异常!", e);
            return false;
        }

    }

    /**
     * 获取部门列表
     *
     * @param departmentId 部门id。获取指定部门及其下的子部门（以及子部门的子部门等等，递归）。 如果不填，默认获取全量组织架构
     * @return 部门列表
     */
    public static synchronized List<Department> listDepartments(Config config, Integer departmentId) {
        WorkAccessToken token = getOrFlushToken(config);
        if (token == null) {
            log.info("获取企业微信部门列表-获取 TOEKN 为空!");
            return null;
        }
        String url = String.format(config.getListDepartmentUrl(), token.getAccessToken(), departmentId);
        try (
                Response response = OKHTTP_CLIENT.newCall(new Request.Builder().url(url).build()).execute()
        ) {
            ResponseBody body = response.body();
            if (Objects.isNull(body)) {
                log.info("获取企业微信部门列表-返回结果为空!");
                return null;
            }
            String resp = body.string();
            log.info("listDepartmentUrl:{}", url);
            log.info("listDepartmentRes:{}", resp);
            if (!StringUtils.isNotBlank(resp)) {
                log.info("获取企业微信部门列表-返回结果为空!");
                return null;
            }
            try {
                JSONObject jsonObject = JSONObject.parseObject(resp);
                if (jsonObject == null || !jsonObject.containsKey("department") || jsonObject.getInteger("errcode") != 0) {
                    log.info("获取企业微信部门列表-返回结果为空!");
                    return null;
                }
                return jsonObject.getJSONArray("department").toJavaList(Department.class);
            } catch (Exception e) {
                log.error("获取企业微信部门列表时异常!", e);
                return null;
            }
        } catch (Exception e) {
            log.error("获取企业微信部门列表时异常!", e);
            return null;
        }
    }

    /**
     * 获取部门成员列表
     *
     * @param departmentId 部门编号
     * @return 用户列表
     */
    public static synchronized List<User> listUsersOfDepartment(Config config, Integer departmentId) {
        WorkAccessToken token = getOrFlushToken(config);
        if (token == null) {
            log.info("获取企业微信部门成员列表-获取 TOEKN 为空!");
            return null;
        }
        String url = String.format(config.getListUserOfDepartmentUrl(), token.getAccessToken(), departmentId);
        try (
                Response response = OKHTTP_CLIENT.newCall(new Request.Builder().url(url).build()).execute()
        ) {
            ResponseBody body = response.body();
            if (Objects.isNull(body)) {
                log.info("获取企业微信部门成员列表-返回结果为空!");
                return null;
            }
            String resp = body.string();
            log.info("listUserOfDepartmentUrl:{}", url);
            log.info("listUserOfDepartmentRes:{}", resp);
            if (!StringUtils.isNotBlank(resp)) {
                log.info("获取企业微信部门成员列表-返回结果为空!");
                return null;
            }
            try {
                JSONObject jsonObject = JSONObject.parseObject(resp);
                if (jsonObject == null || !jsonObject.containsKey("userlist") || jsonObject.getInteger("errcode") != 0) {
                    log.info("获取企业微信部门成员列表-返回结果为空!");
                    return null;
                }
                return jsonObject.getJSONArray("userlist").toJavaList(User.class);
            } catch (Exception e) {
                log.error("获取企业微信部门成员列表时异常!", e);
                return null;
            }
        } catch (Exception e) {
            log.error("获取企业微信部门成员列表时异常!", e);
            return null;
        }
    }


    /**
     * 获取或刷新token <br>
     * 1. 从缓存中获取token <br>
     * 2. 如果 token 存在且未过期, 直接返回token <br>
     * 3. 如果 token 不存在或已过期, 请求企业微信获取新的token <br>
     * 3. 将获取到的token存入缓存 <br>
     * 4. 返回token
     * @return token
     */
    private static WorkAccessToken getOrFlushToken(Config config) {
        MapApi tokenCacheMap = FerryCache.proManager().map(CACHE_KEY);
        WorkAccessToken token = (WorkAccessToken) tokenCacheMap.hashGet(TOKEN_KEY);
        if (token != null && token.isUnExpired()) return token;
        synchronized (WeChatUtil.class) {
            try {
                token = (WorkAccessToken) tokenCacheMap.hashGet(TOKEN_KEY);
                if (token != null && token.isUnExpired()) return token;
                token = requestToken(config);
                if (token != null) {
                    tokenCacheMap.hashSet(TOKEN_KEY, token);
                }
            } catch (Exception e) {
                log.error("获取或刷新token时异常!", e);
            }
        }
        return token;
    }

    /**
     * 请求企业微信获取token
     * @return access token
     */
    private static WorkAccessToken requestToken(Config config) {
        String url = String.format(config.getAccessTokenUrl(), config.getCorpId(), config.getSecret());
        String resp;
        try (
                Response response = OKHTTP_CLIENT.newCall(new Request.Builder().url(url).build()).execute()
        ) {
            ResponseBody body = response.body();
            if (Objects.isNull(body)) {
                log.error("请求企业微信[url:" + url + "]获取ACCESS_TOKEN异常!");
                return null;
            }
            resp = body.string();
            log.info("accessTokenUrl:{}", url);
            log.info("accessTokenRes:{}", resp);
        } catch (Exception e) {
            log.error("请求企业微信[url:{}]获取ACCESS_TOKEN异常!", url, e);
            return null;
        }
        if (!StringUtils.isNotBlank(resp)) {
            log.info("请求企业微信获取ACCESS_TOKEN为空!");
            return null;
        }

        try {
            WorkAccessToken token = OBJECT_MAPPER.readValue(resp, WorkAccessToken.class);
            if (token == null || !token.isSuccess()) {
                log.info("请求企业微信获取ACCESS_TOKEN失败!-TOKEN:{}", resp);
                return null;
            }
            return token;
        } catch (JsonProcessingException e) {
            log.error("请求企业微信[url:{}]获取ACCESS_TOKEN异常!", url, e);
            return null;
        }
    }


    /**
     * 企业微信token
     */
    private static class WorkAccessToken implements Serializable {
        private static final long serialVersionUID = -1704884833558543917L;
        /**
         * 数据示例
         * {
         * "errcode": 0,
         * "errmsg": "ok",
         * "access_token": "hyVmxRCyYUAH0...",
         * "expires_in": 7200
         * }
         */
        //成功状态码
        private static final Integer SUCCESS_CODE = 0;
        private static final Integer PERIOD = 60;//token 过期缓冲时间-s

        /**
         * 状态码
         * 出错返回码，为0表示成功，非0表示调用失败
         *
         * @see "https://elinkuat.spic.com.cn/api/doc#10649"
         */
        @JsonProperty("errcode")
        private Integer errCode;
        //消息 返回码提示语
        @JsonProperty("errmsg")
        private String errMsg;
        //token 获取到的凭证，最长为512字节
        @JsonProperty("access_token")
        private String accessToken;
        //过期时间 凭证的有效时间（秒）
        @JsonProperty("expires_in")
        private Long expiresIn;
        //创建时间 秒
        private Long createTime = TimeUnit.SECONDS.toSeconds(System.currentTimeMillis());

        //是否成功
        public boolean isSuccess() {
            return this.errCode != null && this.errCode.equals(SUCCESS_CODE);
        }

        //是否过期-> true 未过期   false 过期
        public boolean isUnExpired() {
            return TimeUnit.SECONDS.toSeconds(System.currentTimeMillis()) + PERIOD
                    <= (expiresIn + createTime);
        }

        /**********************************get & set******************************/

        public Integer getErrCode() {
            return errCode;
        }

        public void setErrCode(Integer errCode) {
            this.errCode = errCode;
        }

        public String getErrMsg() {
            return errMsg;
        }

        public void setErrMsg(String errMsg) {
            this.errMsg = errMsg;
        }

        public String getAccessToken() {
            return accessToken;
        }

        public void setAccessToken(String accessToken) {
            this.accessToken = accessToken;
        }

        public Long getExpiresIn() {
            return expiresIn;
        }

        public void setExpiresIn(Long expiresIn) {
            this.expiresIn = expiresIn;
        }

        public Long getCreateTime() {
            return createTime;
        }

        public void setCreateTime(Long createTime) {
            this.createTime = createTime;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            WorkAccessToken that = (WorkAccessToken) o;
            return Objects.equals(errCode, that.errCode) && Objects.equals(errMsg, that.errMsg) && Objects.equals(accessToken, that.accessToken) && Objects.equals(expiresIn, that.expiresIn) && Objects.equals(createTime, that.createTime);
        }

        @Override
        public int hashCode() {
            return Objects.hash(errCode, errMsg, accessToken, expiresIn, createTime);
        }

        @Override
        public String toString() {
            return "WorkAccessToken{" +
                    "errCode=" + errCode +
                    ", errMsg='" + errMsg + '\'' +
                    ", accessToken='" + accessToken + '\'' +
                    ", expiresIn=" + expiresIn +
                    ", createTime=" + createTime +
                    '}';
        }
    }

    /**
     * 企业微信消息返回
     */
    private static class WorkMsgResp implements Serializable {
        /**
         * {
         * "errcode" : 0,
         * "errmsg" : "ok",
         * "invaliduser" : "UserID1", // 不区分大小写，返回的列表都统一转为小写
         * "invalidparty" : "PartyID1",
         * "msgid" : "xxxxxxxxxx"
         * }
         */

        //成功状态码
        private static final Integer SUCCESS_CODE = 0;

        /**
         * 状态码
         * 出错返回码，为0表示成功，非0表示调用失败
         *
         * @see "https://elinkuat.spic.com.cn/api/doc#10649"
         */
        @JsonProperty("errcode")
        private Integer errCode;
        //消息 返回码提示语
        @JsonProperty("errmsg")
        private String errMsg;

        @JsonProperty("invaliduser")
        private String invalidUser;

        @JsonProperty("invalidparty")
        private String invalidParty;

        @JsonProperty("msgid")
        private String msgId;

        //是否成功
        public boolean isSuccess() {
            return this.errCode != null && this.errCode.equals(SUCCESS_CODE);
        }

        /**********************************get & set******************************/
        public String getErrMsg() {
            return errMsg;
        }

        public void setErrMsg(String errMsg) {
            this.errMsg = errMsg;
        }

        public String getInvalidUser() {
            return invalidUser;
        }

        public void setInvalidUser(String invalidUser) {
            this.invalidUser = invalidUser;
        }

        public String getInvalidParty() {
            return invalidParty;
        }

        public void setInvalidParty(String invalidParty) {
            this.invalidParty = invalidParty;
        }

        public String getMsgId() {
            return msgId;
        }

        public void setMsgId(String msgId) {
            this.msgId = msgId;
        }

        @Override
        public String toString() {
            return "WorkResp{" +
                    "errCode=" + errCode +
                    ", errMsg='" + errMsg + '\'' +
                    ", invalidUser='" + invalidUser + '\'' +
                    ", invalidParty='" + invalidParty + '\'' +
                    ", msgId='" + msgId + '\'' +
                    '}';
        }
    }

    /**
     * 企业微信部门
     */
    public static class Department implements Serializable {
        //        {
//                "id": 2,
//                "name": "广州研发中心",
//                "name_en": "RDGZ",
//                "department_leader":["zhangsan","lisi"],
//                "parentid": 1,
//                "order": 10
//        }

        /**
         * 部门编号
         */
        @JsonProperty("id")
        private Integer id;
        /**
         * 部门名称
         */
        @JsonProperty("name")
        private String name;
        /**
         * 部门名称(英文)
         */
        @JsonProperty("name_en")
        private String nameEn;
        /**
         * 部门名称(英文)
         */
        @JsonProperty("department_leader")
        private List<String> departmentLeader;
        /**
         * 父元素编号
         */
        @JsonProperty("parentid")
        private Integer parentId;
        /**
         * 排序编号
         */
        @JsonProperty("order")
        private Integer order;

        public Integer getId() {
            return id;
        }

        public void setId(Integer id) {
            this.id = id;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getNameEn() {
            return nameEn;
        }

        public void setNameEn(String nameEn) {
            this.nameEn = nameEn;
        }

        public List<String> getDepartmentLeader() {
            return departmentLeader;
        }

        public void setDepartmentLeader(List<String> departmentLeader) {
            this.departmentLeader = departmentLeader;
        }

        public Integer getParentId() {
            return parentId;
        }

        public void setParentId(Integer parentId) {
            this.parentId = parentId;
        }

        public Integer getOrder() {
            return order;
        }

        public void setOrder(Integer order) {
            this.order = order;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Department that = (Department) o;
            return Objects.equals(id, that.id) && Objects.equals(name, that.name) && Objects.equals(nameEn, that.nameEn) && Objects.equals(departmentLeader, that.departmentLeader) && Objects.equals(parentId, that.parentId) && Objects.equals(order, that.order);
        }

        @Override
        public int hashCode() {
            return Objects.hash(id, name, nameEn, departmentLeader, parentId, order);
        }

        @Override
        public String toString() {
            return "Department{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    ", nameEn='" + nameEn + '\'' +
                    ", departmentLeader=" + departmentLeader +
                    ", parentId=" + parentId +
                    ", order=" + order +
                    '}';
        }
    }

    /**
     * 企业微信用户
     */
    public static class User implements Serializable {
//        {
//                "userid": "zhangsan",
//                "name": "张三",
//                "department": [1, 2],
//                "open_userid": "xxxxxx"
//        }

            /**
            * 成员UserID。对应管理端的帐号
            */
            @JsonProperty("userid")
            private String userId;
            /**
            * 成员名称
            */
            @JsonProperty("name")
            private String name;
            /**
            * 成员所属部门id列表
            */
            @JsonProperty("department")
            private List<Integer> department;
            /**
            * 全局唯一。对于同一个服务商，不同应用获取到企业内同一个成员的open_userid是相同的，最多64个字节。仅第三方应用可获取
            */
            @JsonProperty("open_userid")
            private String openUserId;

            public String getUserId() {
                return userId;
            }

            public void setUserId(String userId) {
                this.userId = userId;
            }

            public String getName() {
                return name;
            }

            public void setName(String name) {
                this.name = name;
            }

            public List<Integer> getDepartment() {
                return department;
            }

            public void setDepartment(List<Integer> department) {
                this.department = department;
            }

            public String getOpenUserId() {
                return openUserId;
            }

            public void setOpenUserId(String openUserId) {
                this.openUserId = openUserId;
            }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            User user = (User) o;
            return Objects.equals(userId, user.userId) && Objects.equals(name, user.name) && Objects.equals(department, user.department) && Objects.equals(openUserId, user.openUserId);
        }

        @Override
        public int hashCode() {
            return Objects.hash(userId, name, department, openUserId);
        }

        @Override
        public String toString() {
            return "User{" +
                    "userId='" + userId + '\'' +
                    ", name='" + name + '\'' +
                    ", department=" + department +
                    ", openUserId='" + openUserId + '\'' +
                    '}';
        }
    }

    /**
     * 企业微信配置
     */
    public static class Config extends BaseConfigBean implements Serializable {
        private String secret;
        private String corpId;
        private String agentId;
        // https://10.252.1.8/cgi-bin/gettoken?corpid=%s&corpsecret=%s
        private String accessTokenUrl; //获取access_token
        // https://10.252.1.8/cgi-bin/message/send?access_token=%s
        private String messageSendUrl;//给企业微信成员发送消息
        // https://qyapi.weixin.qq.com/cgi-bin/department/list?access_token=%s&id=%s
        private String listDepartmentUrl; // 列举部门列表
        // https://qyapi.weixin.qq.com/cgi-bin/user/simplelist?access_token=ACCESS_TOKEN&department_id=DEPARTMENT_ID
        private String listUserOfDepartmentUrl; // 获取部门成员

        public String getSecret() {
            return secret;
        }

        public void setSecret(String secret) {
            this.secret = secret;
        }

        public String getCorpId() {
            return corpId;
        }

        public void setCorpId(String corpId) {
            this.corpId = corpId;
        }

        public String getAccessTokenUrl() {
            return accessTokenUrl;
        }

        public void setAccessTokenUrl(String accessTokenUrl) {
            this.accessTokenUrl = accessTokenUrl;
        }

        public String getMessageSendUrl() {
            return messageSendUrl;
        }

        public void setMessageSendUrl(String messageSendUrl) {
            this.messageSendUrl = messageSendUrl;
        }

        public String getListDepartmentUrl() {
            return listDepartmentUrl;
        }

        public void setListDepartmentUrl(String listDepartmentUrl) {
            this.listDepartmentUrl = listDepartmentUrl;
        }

        public String getListUserOfDepartmentUrl() {
            return listUserOfDepartmentUrl;
        }

        public void setListUserOfDepartmentUrl(String listUserOfDepartmentUrl) {
            this.listUserOfDepartmentUrl = listUserOfDepartmentUrl;
        }

        public String getAgentId() {
            return agentId;
        }

        public void setAgentId(String agentId) {
            this.agentId = agentId;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Config config = (Config) o;
            return Objects.equals(secret, config.secret) && Objects.equals(corpId, config.corpId) && Objects.equals(agentId, config.agentId) && Objects.equals(accessTokenUrl, config.accessTokenUrl) && Objects.equals(messageSendUrl, config.messageSendUrl) && Objects.equals(listDepartmentUrl, config.listDepartmentUrl) && Objects.equals(listUserOfDepartmentUrl, config.listUserOfDepartmentUrl);
        }

        @Override
        public int hashCode() {
            return Objects.hash(secret, corpId, agentId, accessTokenUrl, messageSendUrl, listDepartmentUrl, listUserOfDepartmentUrl);
        }

        @Override
        public String toString() {
            return "Config{" +
                    "secret='" + secret + '\'' +
                    ", corpId='" + corpId + '\'' +
                    ", agentId='" + agentId + '\'' +
                    ", accessTokenUrl='" + accessTokenUrl + '\'' +
                    ", messageSendUrl='" + messageSendUrl + '\'' +
                    ", listDepartmentUrl='" + listDepartmentUrl + '\'' +
                    ", listUserOfDepartmentUrl='" + listUserOfDepartmentUrl + '\'' +
                    '}';
        }
    }

//    public static void main(String[] args) {
//        WeChatUtil weChatUtil = new WeChatUtil();
//        Config config = new Config();
//        config.secret = "6eqa8hRx9DEUR6ecbnkGtuLk8cjrw4TgGM1Oh6ygBh4";
//        config.corpId = "ww7660541f094cb144";
//        config.accessTokenUrl = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s";
//        config.messageSendUrl = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=%s";
//        config.listDepartmentUrl = "https://qyapi.weixin.qq.com/cgi-bin/department/list?access_token=%s&id=%s";

//        weChatUtil.config = config;
//        List<Department> departments = weChatUtil.listDepartments(null);
//        System.out.println(departments);
//        List<User> users = weChatUtil.listUsersOfDepartment(1);
//        System.out.println(users);
//    }
}
